﻿/* 
   Copyright 2011 LUCA Studios LLC & John Evans

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

namespace LUCAStudios.Silverlight.Controls
{
    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using DragDrop;
    using Windows;

    /// <summary>
    /// This class provides a base for window-type controls in the LUCA UI framework
    /// </summary>
    [TemplatePart(Name = BorderRight, Type = typeof(Border))]
    [TemplatePart(Name = BorderLeft, Type = typeof(Border))]
    [TemplatePart(Name = BorderTop, Type = typeof(Border))]
    [TemplatePart(Name = BorderBottom, Type = typeof(Border))]
    [TemplatePart(Name = BorderTopRight, Type = typeof(Border))]
    [TemplatePart(Name = BorderTopLeft, Type = typeof(Border))]
    [TemplatePart(Name = BorderBottomRight, Type = typeof(Border))]
    [TemplatePart(Name = BorderBottomLeft, Type = typeof(Border))]
    [TemplatePart(Name = BorderChrome, Type = typeof(Border))]
    [TemplatePart(Name = BorderContent, Type = typeof(Border))]
    [TemplatePart(Name= ScrollViewerContent, Type=typeof(ScrollViewer))]
    public abstract class WindowBase : ContainerBase, IWindow, IDragDropSource
    {
        #region fields
        private bool _manualDragFlag;
        private Point _dragClickOrigin;

        private const double MouseUndockSensitivity = 15; //the distance in pixels, which the mouse must move for a docked window to be undocked
        private const double MinWindowHeight = 150;
        private const double MinWindowWidth = 150;
        private const double DefaultHeight = 300;
        private const double DefaultWidth = 300;

        private Border _borderRight,
                       _borderLeft,
                       _borderTop,
                       _borderBottom,
                       _borderTopLeft,
                       _borderTopRight,
                       _borderBottomLeft,
                       _borderBottomRight,
                       _borderContent;

        private Border _borderChrome;

        private ScrollViewer _contentScrollViewer;

        private const string BorderRight = "borderRight";
        private const string BorderLeft = "borderLeft";
        private const string BorderTop = "borderTop";
        private const string BorderBottom = "borderBottom";
        private const string BorderTopRight = "borderTopRight";
        private const string BorderTopLeft = "borderTopLeft";
        private const string BorderBottomRight = "borderBottomRight";
        private const string BorderBottomLeft = "borderBottomLeft";
        private const string BorderChrome = "borderChrome";
        private const string BorderContent = "borderContent";
        private const string ScrollViewerContent = "contentScrollViewer";


        #endregion

        #region private/internal



        /// <summary>
        /// Called when the LUCA control is loaded
        /// </summary>
        protected override void OnLoaded()
        {
            base.OnLoaded();

            //if this is a free floating window, we are going to define the Height/Width if not already defined...
            if (ContainerContext.Equals(LayoutContext) &&
                (VisualState == WindowLayoutState.Floating))
            {
                Width = (Width.Equals(double.NaN) || Width < MinWindowWidth) ? DefaultHeight : Width;
                Height = (Height.Equals(double.NaN) || Height < MinWindowHeight) ? DefaultWidth : Height;
            }

            InitializeControlReferences();

            SetVisualState(VisualState);

            TransformX(XOffset); //automatically transforms to current YOffset

            LayoutContext
                .WindowManipulator
                .RegisterWindow(this);

            RegisterDockEvents();

            if (!_manualDragFlag) return;
            _manualDragFlag = false;

            if (_dragClickOrigin != default(Point))
            {
                LayoutContext
                    .WindowManipulator
                    .ForceWindowDragState(DragHandle,
                                            _dragClickOrigin.X > (Width / 2 + 10)
                                                ? new Point(Width / 2d, _dragClickOrigin.Y)
                                                : _dragClickOrigin);
                _dragClickOrigin = default(Point);
            }
            else
            {
                //TODO: clean this up so MinHeight isn't required for offset
                var layoutPosition = LayoutContext
                                        .GetPosition(LayoutContext.RootVisual);
                LayoutContext
                    .WindowManipulator
                    .ForceWindowDragState(DragHandle,
                                            new Point(Width / 2d - layoutPosition.X, DragHandle.MinHeight / 2d - layoutPosition.Y));
            }
        }

        /// <summary>
        /// Called when the LUCA control is unloaded
        /// </summary>
        protected override void OnUnLoaded()
        {
            base.OnUnLoaded();

            UnRegisterDockEvents();

            LayoutContext
                .WindowManipulator
                .UnregisterWindow(this);
        }

        /// <summary>
        /// Registers dock events
        /// </summary>
        protected void RegisterDockEvents()
        {
            if (VisualState != WindowLayoutState.Docked || !IsDetachable) return;

            DragHandle.MouseLeftButtonDown += DragHandleMouseLeftButtonDown;
        }

        /// <summary>
        /// unregisters dock events
        /// </summary>
        protected void UnRegisterDockEvents()
        {
            DragHandle.MouseLeftButtonDown -= DragHandleMouseLeftButtonDown;
        }

        private void DragHandleMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var layoutPosition = LayoutContext
                                     .GetPosition(LayoutContext.RootVisual);

            var mousePosition = e.GetPosition(this);

            _dragClickOrigin = new Point(mousePosition.X - layoutPosition.X, mousePosition.Y - layoutPosition.Y);
            SetPeelOutEvents();
            e.Handled = true;
        }

        private void SetPeelOutEvents()
        {
            DragHandle.MouseLeave += DragHandleMouseLeave;
            DragHandle.MouseMove += DragHandleMouseMove;
            DragHandle.MouseLeftButtonUp += DragHandleMouseLeftButtonUp;
        }

        void DragHandleMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            ClearPeelOutEvents();
        }

        void DragHandleMouseMove(object sender, MouseEventArgs e)
        {
            if (_dragClickOrigin.ManhattenDistanceTo(e.GetPosition(this)) <= MouseUndockSensitivity) return;

            DockedToFloating();
        }

        void DragHandleMouseLeave(object sender, MouseEventArgs e)
        {
            DockedToFloating();
        }

        private void ClearPeelOutEvents()
        {
            DragHandle.MouseLeave -= DragHandleMouseLeave;
            DragHandle.MouseMove -= DragHandleMouseMove;
            DragHandle.MouseLeftButtonUp -= DragHandleMouseLeftButtonUp;
        }

        private void DockedToFloating()
        {
            ClearPeelOutEvents();

            this.AbsoluteTransformPositionTo(new Point(0, 0), LayoutContext);

            Height = ContainerContext.LastKnownHeight;
            Width = ContainerContext.LastKnownWidth;

            ContainerContext.RemoveChild(this);

            RenderTransform = null;

            YOffset = 0;
            XOffset = 0;

            IsDragEnabled = true;

            HorizontalAlignment = HorizontalAlignment.Left;
            VerticalAlignment = VerticalAlignment.Top;

            VisualState = WindowLayoutState.Floating;

            AddAndDrag(LayoutContext);
        }

        /// <summary>
        /// Adds itself to the root LUCALayoutPanel and puts into manual drag mode so that
        /// this user is automatically dragging the window.
        /// This mode is useful for peal-away operations.  MUST be called before the Loaded event fires.
        /// </summary>
        /// <param name="rootLayoutPanel"></param>
        internal void AddAndDrag(LUCALayoutPanel rootLayoutPanel)
        {
            _manualDragFlag = true;
            rootLayoutPanel.AddChild(this);
            ContainerContext = LayoutContext;
        }

        private static void OnHorizontalScrollBarVisibilityChanging(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var o = d as WindowBase;
            if (o == default(WindowBase)) return;
            o._contentScrollViewer.HorizontalScrollBarVisibility = (ScrollBarVisibility)e.NewValue;
        }

        private static void OnVerticalScrollBarVisibilityChanging(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var o = d as WindowBase;
            if (o == default(WindowBase)) return;
            o._contentScrollViewer.VerticalScrollBarVisibility = (ScrollBarVisibility)e.NewValue;
        }

        /// <summary>
        /// Get references to all the elements needed for the IWindow interface
        /// </summary>
        protected void InitializeControlReferences()
        {
            //if (DragHandle != default(FrameworkElement)) return;

            //DragHandle = this.FindElement<FrameworkElement>("borderChrome");
            //HeightResizerTop = this.FindElement<Border>("borderTop");
            //HeightResizerBottom = this.FindElement<Border>("borderBottom");
            //WidthResizerLeft = this.FindElement<Border>("borderLeft");
            //WidthResizerRight = this.FindElement<Border>("borderRight");
            //ResizerTopLeft = this.FindElement<Border>("borderTopLeft");
            //ResizerTopRight = this.FindElement<Border>("borderTopRight");
            //ResizerBottomLeft = this.FindElement<Border>("borderBottomLeft");
            //ResizerBottomRight = this.FindElement<Border>("borderBottomRight");
            //ContentScrollViewer = this.FindElement<ScrollViewer>("contentScrollViewer");
        }

        private static void OnXChanging(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var that = o as WindowBase;
            if (that == null) return;
            var newX = (double)e.NewValue;
            if (newX < 0) return;

            that.TransformX(newX);
        }

        private static void OnYChanging(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var that = o as WindowBase;
            if (that == null) return;
            var newY = (double)e.NewValue;
            if (newY < 0) return;

            that.TransformY(newY);
        }

        private void TransformX(double newValue)
        {
            if (LayoutContext == null) return;
            if (VisualState != WindowLayoutState.Floating) return;

            this.AbsoluteTransformPositionTo(new Point(newValue, YOffset), LayoutContext);
        }

        private void TransformY(double newValue)
        {
            if (LayoutContext == null) return;
            if (VisualState != WindowLayoutState.Floating) return;

            this.AbsoluteTransformPositionTo(new Point(XOffset, newValue), LayoutContext);
        }

        private static void OnVisualStateChanging(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            var i = o as LUCATabWindow;
            if (i == null) return;

            i.SetVisualState((WindowLayoutState)e.NewValue);
        }

        /// <summary>
        /// Called by VisualState dependency property when VisualState is changing.  Should generally not be called directly.
        /// </summary>
        /// <param name="newState">The state to set the control's Visual State to.</param>
        protected virtual void SetVisualState(WindowLayoutState newState)
        {
            switch (newState)
            {
                case WindowLayoutState.Floating:
                    VisualStateManager.GoToState(this, "Normal", false);
                    break;
                case WindowLayoutState.Docked:
                    VisualStateManager.GoToState(this, "Docked", false);
                    break;
                case WindowLayoutState.NoUI:
                    VisualStateManager.GoToState(this, "NoUI", false);
                    break;
                case WindowLayoutState.Hidden:
                    throw new NotSupportedException();
            }
        }

        #endregion

        #region properties
        /// <summary>
        /// Backing property for the VisualState Dependency Property
        /// </summary>
        public static readonly DependencyProperty VisualStateProperty =
            DependencyProperty.Register("VisualState", typeof(WindowLayoutState), typeof(WindowBase),
                        new PropertyMetadata(WindowLayoutState.Floating, OnVisualStateChanging));
        /// <summary>
        /// Backing property for the IsResizeEnabled Dependency Property
        /// </summary>
        public static readonly DependencyProperty IsResizeEnabledProperty =
            DependencyProperty.Register("IsResizeEnabled", typeof(bool), typeof(WindowBase),
                                        new PropertyMetadata(true));
        /// <summary>
        /// Backing property for the IsDragEnabled Dependency Property
        /// </summary>
        public static readonly DependencyProperty IsDragEnabledProperty =
            DependencyProperty.Register("IsDragEnabled", typeof(bool), typeof(WindowBase),
                                        new PropertyMetadata(true));
        /// <summary>
        /// Backing property for the WindowID Dependency Property
        /// </summary>
        public static readonly DependencyProperty WindowIDProperty =
            DependencyProperty.Register("WindowID", typeof(string), typeof(WindowBase),
                                        new PropertyMetadata(String.Empty));
        /// <summary>
        /// Backing property for the WindowIcon Dependency Property
        /// </summary>
        public static readonly DependencyProperty WindowIconProperty =
            DependencyProperty.Register("WindowIcon", typeof(object), typeof(WindowBase), new PropertyMetadata(null));
        /// <summary>
        /// Backing property for the CurrentContent Dependency Property
        /// </summary>
        public static readonly DependencyProperty CurrentContentProperty =
            DependencyProperty.Register("CurrentContent", typeof(object), typeof(WindowBase),
                                new PropertyMetadata(null));
        /// <summary>
        /// Backing property for the IsAttachable Dependency Property
        /// </summary>
        public static readonly DependencyProperty IsAttachableProperty =
            DependencyProperty.Register("IsAttachable", typeof(bool), typeof(WindowBase), new PropertyMetadata(true));
        /// <summary>
        /// Backing property for the IsDetachable Dependency Property
        /// </summary>
        public static readonly DependencyProperty IsDetachableProperty =
            DependencyProperty.Register("IsDetachable", typeof(bool), typeof(WindowBase),
                                               new PropertyMetadata(true));
        /// <summary>
        /// Backing property for the XOffset Dependency Property
        /// </summary>
        public static readonly DependencyProperty XOffsetProperty =
            DependencyProperty.Register("XOffset", typeof(double), typeof(WindowBase),
                                new PropertyMetadata(0d, OnXChanging));

        /// <summary>
        /// Backing property for the YOffset Dependency Property
        /// </summary>
        public static readonly DependencyProperty YOffsetProperty =
            DependencyProperty.Register("YOffset", typeof(double), typeof(WindowBase),
                                        new PropertyMetadata(0d, OnYChanging));

        /// <summary>
        /// Backing property for the VerticalScrollBarVisibility Dependency Property
        /// </summary>
        public static readonly DependencyProperty VerticalScrollBarVisibilityProperty =
            DependencyProperty.Register("VerticalScrollBarVisibility", typeof(ScrollBarVisibility), typeof(WindowBase),
                                               new PropertyMetadata(ScrollBarVisibility.Auto, OnVerticalScrollBarVisibilityChanging));

        /// <summary>
        /// Backing property for the HorizontalScrollBarVisibility Dependency Property
        /// </summary>
        public static readonly DependencyProperty HorizontalScrollBarVisibilityProperty =
           DependencyProperty.Register("HorizontalScrollBarVisibility", typeof(ScrollBarVisibility), typeof(WindowBase),
                                               new PropertyMetadata(ScrollBarVisibility.Auto, OnHorizontalScrollBarVisibilityChanging));


        /// <summary>
        /// Dependency property which Gets or Sets the control's current visual state using 
        /// strongly-typed WindowLayoutState enum.
        /// </summary>
        public WindowLayoutState VisualState
        {
            get { return (WindowLayoutState)GetValue(VisualStateProperty); }
            set { SetValue(VisualStateProperty, value); }
        }

        /// <summary>
        /// Dependency property which Gets or sets a boolean value indicated whether or not 
        /// the user should be able to resize the control.
        /// </summary>
        public bool IsResizeEnabled
        {
            get { return (bool)GetValue(IsResizeEnabledProperty); }
            set { SetValue(IsResizeEnabledProperty, value); }
        }

        /// <summary>
        /// Dependency property which Gets or sets a boolean value indicating whether or not 
        /// the user is able to drag the control.
        /// </summary>
        public bool IsDragEnabled
        {
            get { return (bool)GetValue(IsDragEnabledProperty); }
            set { SetValue(IsDragEnabledProperty, value); }
        }

        /// <summary>
        /// Dependency property which Gets or sets the unique ID of the control.  Provided as a 
        /// convenience identifier for users of the control.
        /// </summary>
        public string WindowID
        {
            get { return (string)GetValue(WindowIDProperty); }
            set { SetValue(WindowIDProperty, value); }
        }

        /// <summary>
        /// Dependency property which Gets or Sets the content that Will appear in the left-hand side of the window's chrome.
        /// </summary>
        public object WindowIcon
        {
            get { return GetValue(WindowIconProperty); }
            set { SetValue(WindowIconProperty, value); }
        }

        /// <summary>
        /// Dependency property indicating whether this window can attach itself to other attachable areas.
        /// </summary>
        public bool IsAttachable
        {
            get { return (bool)GetValue(IsAttachableProperty); }
            set { SetValue(IsAttachableProperty, value); }
        }

        /// <summary>
        /// Dependency proeprty which Gets or Sets the content that is to be displayed in the main area of the window.
        /// </summary>
        public object CurrentContent
        {
            get { return GetValue(CurrentContentProperty); }
            set { SetValue(CurrentContentProperty, value); }
        }

        /// <summary>
        /// Dependency propeerty which Gets or Sets a boolean value indicating whether this window 
        /// can detach itself from it's parent container and become a floating window.
        /// </summary>
        public bool IsDetachable
        {
            get { return (bool)GetValue(IsDetachableProperty); }
            set { SetValue(IsDetachableProperty, value); }
        }

        /// <summary>
        /// Dependency property which Gets or Sets the HorizontalScrollBarVisibility property of the Window's content ScrollBar
        /// </summary>
        public ScrollBarVisibility HorizontalScrollBarVisibility
        {
            get { return (ScrollBarVisibility)GetValue(HorizontalScrollBarVisibilityProperty); }
            set { SetValue(HorizontalScrollBarVisibilityProperty, value); }
        }

        /// <summary>
        /// Dependency property which Gets or Sets the VerticalScrollBarVisibility property of the Window's content ScrollBar
        /// </summary>
        public ScrollBarVisibility VerticalScrollBarVisibility
        {
            get { return (ScrollBarVisibility)GetValue(VerticalScrollBarVisibilityProperty); }
            set { SetValue(VerticalScrollBarVisibilityProperty, value); }
        }

        /// <summary>
        /// Dependency property which specifies the X location of the Window.
        /// Only works if window is in a Floating state.
        /// </summary>
        public double XOffset
        {
            get { return (double)GetValue(XOffsetProperty); }
            set { SetValue(XOffsetProperty, value); }
        }

        /// <summary>
        /// Dependency property which specifies the Y location of the control.
        /// Only works if window is in a Floating state.
        /// </summary>
        public double YOffset
        {
            get { return (double)GetValue(YOffsetProperty); }
            set { SetValue(YOffsetProperty, value); }
        }

        #endregion

        #region IWindow Members

        /// <summary>
        /// Provides a boolean indicating whether the WindowManipulation class should honor window drag requests from the user.
        /// </summary>
        public bool DragEnabled
        {
            get { return IsDragEnabled; }
        }

        /// <summary>
        /// The visual element representing the drag handle of the window (usually the "chrome" or title bar)
        /// </summary>
        public FrameworkElement DragHandle { get; protected set; }

        /// <summary>
        /// The visual element representing the resize-bottom operation to the user.  Assign to null if not using.
        /// </summary>
        public FrameworkElement HeightResizerBottom { get; protected set; }

        /// <summary>
        /// The visual element representing the resize-top operation to the user.  Assign to null if not using.
        /// </summary>
        public FrameworkElement HeightResizerTop { get; protected set; }

        /// <summary>
        /// Constrains any resize activity to a minimum height value.
        /// </summary>
        public double MinimumWindowHeight
        {
            get { return MinWindowHeight; }
        }

        /// <summary>
        /// Constrains any resize activity to a minimum width value.
        /// </summary>
        public double MinimumWindowWidth
        {
            get { return MinWindowWidth; }
        }

        /// <summary>
        /// Called when drag operation completes.
        /// </summary>
        public void OnDragEnded()
        {
            // IsHitTestVisible = true;

        }

        /// <summary>
        /// Called just prior to when drag operation begins.
        /// </summary>
        public void OnDragStarted()
        {
            OnSelected();
        }

        public void OnResizeStarted()
        {
            //prevent some of the window ui from interfering with the mouseup event
            _borderContent.IsHitTestVisible = false;
            _borderChrome.IsHitTestVisible = false;
        }

        public void OnResizeEnded()
        {
            _borderContent.IsHitTestVisible = true;
            _borderChrome.IsHitTestVisible = true;
        }

        /// <summary>
        /// Provides a boolean indicating whether the WindowManipulation class should honor window resize requests from the user.
        /// </summary>
        public bool ResizeEnabled
        {
            get { return IsResizeEnabled; }
        }

        /// <summary>
        /// The visual element representing the resize-bottom/left operation to the user.  Assign to null if not using.
        /// </summary>
        public FrameworkElement ResizerBottomLeft { get; protected set; }

        /// <summary>
        /// The visual element representing the resize-bottom/right operation to the user.  Assign to null if not using.
        /// </summary>
        public FrameworkElement ResizerBottomRight { get; protected set; }

        /// <summary>
        /// The visual element representing the resize-top/left operation to the user.  Assign to null if not using.
        /// </summary>
        public FrameworkElement ResizerTopLeft { get; protected set; }

        /// <summary>
        /// The visual element representing the resize-top/left operation to the user.  Assign to null if not using.
        /// </summary>
        public FrameworkElement ResizerTopRight { get; protected set; }

        /// <summary>
        /// The visual element representing the resize-left operation to the user.  Assign to null if not using.
        /// </summary>
        public FrameworkElement WidthResizerLeft { get; protected set; }

        /// <summary>
        /// The visual element representing the resize-right operation to the user.  Assign to null if not using.
        /// </summary>
        public FrameworkElement WidthResizerRight { get; protected set; }

        /// <summary>
        /// The visual representation of the window to perform drag/resize transformations on.
        /// </summary>
        public FrameworkElement DragElement
        {
            get { return this; }
        }

        #endregion

        #region IDragDropSource Members

        /// <summary>
        /// Not yet supported
        /// </summary>
        public bool UsePreview
        {
            get { return false; }
        }

        /// <summary>
        /// Not yet supported
        /// </summary>
        public Style PreviewStyle
        {
            get { return null; }
        }

        /// <summary>
        /// Not yet supported
        /// </summary>
        public bool LeaveInPlace
        {
            get { return true; }
        }

        /// <summary>
        /// General purpose property which holds any data that should follow with the dragged control.
        /// Is provided to the IDragDropTarget on a successful drop
        /// Usually this is the set to the dragging object itself, but can be used for passing any type of data
        /// </summary>
        public virtual object DragDropData
        {
            get { return this; }
        }

        /// <summary>
        /// Called when the control is dropped on a IDragDropTarget
        /// </summary>
        /// <param name="target"></param>
        public virtual void OnDropped(IDragDropTarget target)
        {

        }
        #endregion

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            WidthResizerLeft = _borderLeft = GetTemplateChild(BorderLeft) as Border;
            HeightResizerTop = _borderTop = GetTemplateChild(BorderTop) as Border;
            WidthResizerRight = _borderRight = GetTemplateChild(BorderRight) as Border;
            HeightResizerBottom = _borderBottom = GetTemplateChild(BorderBottom) as Border;
            ResizerTopLeft = _borderTopLeft = GetTemplateChild(BorderTopLeft) as Border;
            ResizerTopRight = _borderTopRight = GetTemplateChild(BorderTopRight) as Border;
            ResizerBottomLeft = _borderBottomLeft = GetTemplateChild(BorderBottomLeft) as Border;
            ResizerBottomRight = _borderBottomRight = GetTemplateChild(BorderBottomRight) as Border;
            DragHandle = _borderChrome = GetTemplateChild(BorderChrome) as Border;
            _borderContent = GetTemplateChild(BorderContent) as Border;
            _contentScrollViewer = GetTemplateChild(ScrollViewerContent) as ScrollViewer;
        }

    }
}
